暨上一篇把多個關卡流程設定完,這篇要把傳送門的門前放上一個 boss。然後也一直想做遠程攻擊,所以就放了一個巫師站在門前施放魔法,這樣可以維持距離又能造成玩家的壓力。
這回的重點:
new_demo/src/constants.rs 先引入 Color,新的一組常數定義巫師的遠程設定:
pub const WIZARD_BOSS_ATTACK_RADIUS: f32 = 260.0;
pub const WIZARD_BOSS_CAST_MIN_DISTANCE: f32 = 140.0;
pub const WIZARD_BOSS_PROJECTILE_SPEED: f32 = 360.0;
pub const WIZARD_BOSS_PROJECTILE_LIFETIME: f32 = 2.0;
pub const WIZARD_BOSS_PROJECTILE_SIZE: f32 = 18.0;
pub const WIZARD_BOSS_PROJECTILE_HIT_RADIUS: f32 = 24.0;
pub const WIZARD_BOSS_PROJECTILE_COLOR: Color = Color::srgb(0.72, 0.28, 0.92);
ATTACK_RADIUS:施法上限(傳送門前站著也會被打)。CAST_MIN_DISTANCE:過近時會後退,防止玩家卡在腳邊無限揮空。PROJECTILE_*:速度/壽命/碰撞半徑與視覺尺寸,魔法球顏色直接用 Color::srgb 設成紫色。
素材方面使用新增的 assets/characters/enemies/wizard.png、assets/weapons/enemy/wizard_staff.png,法杖縮放改成 1.0,並把 offset 調到 8.5, -2.0 讓武器貼在右手。
為了讓巫師的魔法球能在 ECS 裡統一管理,new_demo/src/components/enemy.rs 增加兩個 component:
#[derive(Component)]
pub struct BossWizardProjectile {
    pub velocity: Vec2,
    pub damage: i32,
}
#[derive(Component)]
pub struct BossWizardProjectileLifetime {
    pub timer: Timer,
}
魔法球會帶著速度 / 傷害值,搭配 Timer 自動過期。產生時同時加上 LevelEntity,換關即會被清掉。
new_demo/src/systems/enemy.rs 的 boss_wizard_ai_system 原本只是共用近戰巡邏邏輯,現在改成專屬流程:
to_player 和 distance 是整個策略的基礎。距離小於 CAST_MIN_DISTANCE 就以 retreat_dir 往反方向滑動;距離過大則微幅前進,始終保持在 140~260 之間。因為巫師只需左右移動,所以 clamp 只作用在 translation.x。EnemyPatrol 的 bounds()。這樣做的好處是,萬一場景很狹長,巫師還是會遵守房間左右邊界,不會退到牆外。EnemyAttack.cooldown.tick(delta) 後判斷 finished(),若是第一次施法會立即重置冷卻,確保巫師能在看到玩家的瞬間開火。transform.translation 與 direction 傳入,函式內部會根據 WIZARD_BOSS_STAFF_OFFSET_X 和 WIZARD_BOSS_CAST_HEIGHT_OFFSET 把產生點往前、往上偏移,呈現出從法杖頂端發射的視覺效果。patrol.direction = dx.signum() 維持左右 facing 的正負號,後續如果要切換 sprite 甚至決定施法音效,都能沿用這個欄位。這段仍然保留原始巡邏 fallback,若玩家離開觸發/牽制距離,EnemyBehaviorState::Patrolling 會接手,巫師就像一般敵人一樣在房間內來回,避免卡在單一點位等待玩家回頭。
巫師的魔法會在 boss_wizard_projectile_system 中統一更新,相關程式如下:
pub fn boss_wizard_projectile_system(
    mut commands: Commands,
    time: Res<Time>,
    mut projectile_query: Query<(
        Entity,
        &mut Transform,
        &BossWizardProjectile,
        &mut BossWizardProjectileLifetime,
    )>,
    player_target_query: Query<(
        &Transform,
        Option<&Defense>,
    ), (With<Player>, Without<PlayerDead>, Without<BossWizardProjectile>)>,
    mut player_health_query: Query<
        &mut Health,
        (With<Player>, Without<PlayerDead>, Without<BossWizardProjectile>),
    >,
    mut damage_events: EventWriter<PlayerDamagedEvent>,
    mut enemy_attack_events: EventWriter<EnemyAttackHitEvent>,
) { /* ... */ }
velocity 持續向前,BossWizardProjectileLifetime 的 Timer 到期就 despawn,確保不會永久漂浮或在離線畫面耗效能。Without<BossWizardProjectile>,避免和 projectile_query 同時借用 Transform 造成 B0001 錯誤,不然可能會遇到 panic,並且透過條件約束避免重複抓取同一批 component。distance <= WIZARD_BOSS_PROJECTILE_HIT_RADIUS 做圓形範圍判斷。若未來要做更複雜的彈道(例如穿透或爆炸),可以在這邊擴充碰撞邏輯。hits,統一處理 despawn 與扣血,並發出 PlayerDamagedEvent、EnemyAttackHitEvent。這樣 UI、音效、震動等下游行為完全不需修改,只要監聽既有事件即可。同一幀可能有多顆魔法球命中,程式會先把結果放進 hits 陣列,再統一 despawn,確保血量只在成功命中時扣一次,且事件順序穩定。若玩家已死亡,player_health_query.single_mut() 會失敗並落到 else 分支,只把子彈清掉不再寫事件,避免額外 panic。
src/plugins/enemy.rs:把 boss_wizard_projectile_system 加進 Update,放在近戰傷害前面,魔法球命中仍會觸發相同的傷害事件。src/resources/level.rs:第一關 boss_wizards: 1,進遊戲就能立即觀察巫師招式,,目前還在思考 boss 會在哪幾關出現。src/systems/level.rs:產生巫師時把 Name、EnemyAttack 初始值調整好,保持 Fallback 跟巡邏邏輯一樣。
配合上一篇的系統,boss 固定站在傳送門前,只要玩家到一定的距離內,就會開始釋放魔法攻擊玩家。
加上一個遠程攻擊的法師 boss 後,傳送門的威脅感立刻提升許多。不過我預想的會是:
今日程式碼同步至 repo